Klasyfikacja jakości filmu¶
Autorzy¶
Ilya Pauliuk (217344), Adam Ropelewski (217518), Dawid Rychlik (217264), Jakub Szala (217367)
Streszczenie¶
Wykonany przez nas projekt zajmuję się klasyfikacją filmów ze względu na spodziewaną ocenę przez ich odbiorców. W tym celu wykorzystujemy sprawdzone metody klasyfikacji, które mają swoje implementacje w bibliotece scikit-learn. Wymagało to dokładnej obróbki danych.
Większość filmów należy do gorszej grupy. Jedynie koło 10% filmów można nazwać "dobrymi". Osiągamy około 73% dokładności klasyfikacji.
Słowa kluczowe¶
Filmy, GroupLens, klasyfikacja metodami: najbliższych sąsiadów, regresji logistycznej, liniowią SVC, hybrydową; klasteryzacja metodą k-średnich
Wprowadzenie¶
W latach osiemdziesiątych obejrzenie filmu wymagało wizyty w kinie lub ograniczenia się do reperteuru proponowanego przez telewizję. W erze, gdy kino było jeszcze ekscytującą nowością, każdy postęp w dziedzinie kinematografii, czy to w reżyserii czy w efektach specjalnych, sprawiał, że dzieło utrwalało się pozytywnie w pamięci widzów. Te czasy jednak minęły, a coraz to bardziej ekstrawaganckie elementy dokonywane w postprodukcji nie robią już tak dużego wrażenia.
Teraz dostęp do popkultury jest na wyciągnięcie ręki, kilka kliknięć pozwala nam przeglądać niezliczonych biblioteki serwisów streamingowych takich jak Netflix, Max, Hulu, Disney+ etc. W dzisiejszych czasach odbiorcy proponowana jest cały wachlarz produkcji, z których większość nie zapada mu w pamięć. Określenie czy dane dzieło kina wzbudzi u odbiorcy pozytywną reakcję, tym samym zasługując na zakup przez platformę streamingową, jest zadaniem niełatwym. W dodatku biorąc pod uwagę dużą ilość powstałych ekranizacji i tego, że kolejne powstają co roku — czasochłonnym.
W celu stworzenia modelu statystycznego wykorzystujemy dane około 26 mln ocen użytkowników ze strony GroupLens. We wspomnianych danych pojawiły się liczne braki wymagające obsłużenia. Ze względu na dużą objętość danych filmy posiadające braki nie brały udziału w uczeniu modelu.
Po przetworzeniu danych zajeliśmy się klasteryzacją przy pomocy metody k-średnich. Odkryliśmy, że odpowiednia ilość klastrów, to 2. Na tej podstawie powstały grafy wygenerowane przy pomocy Seaborn, które to pomogły w ocenie użyteczności zmiennych uczących.
Na końcu tworzymy cztery modele klasyfikacji i testujemy ich efektywność. Ich potencjalnym zastosowaniem może być pomoc właścicielom wytwórni płyt DVD, serwisów streamingowych i kin, w określeniu, do których filmów prawa powinni wykupić, aby jak najlepiej zadowolić swojego klienta.
Opis danych wejściowych¶
Dane pozyskaliśmy z serwisu kaggle, zawierały one około 26 mln ocen użytkowników zebranych przez GroupLens. Należało zsumować oceny konkretnego dzieła i podzielić je przez ich ilość ocen, a następnie przydzielić do konkretnych filmów. W ten sposób uzyskaliśmy średnią ocenę dla każdego z filmów. Posłuży ona jako zmienna y — wartość walidacyjna.
Kod przygotowujący dane znajduję się poniżej, a także w pliku main_data_prep.py. Nie da się go uruchomić w tym zeszycie Jupyter, ponieważ znacznie by to spowolniło uruchamianie kolejnych komórek ze względu na ilość danych jakie on importuje ~ 1GB plików csv.
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import ast
import numpy as np
from constants import COUNTRY_CODES
def process_genres(movies_metadata):
temp_list_of_genres_per_movie = []
temp_list_of_genres_per_movie.extend(movies_metadata["genres"].to_list())
for i in range(len(movies_metadata["genres"])):
if temp_list_of_genres_per_movie[i] != "[]":
temp_list_of_genres_per_movie[i] = ast.literal_eval(
movies_metadata["genres"].to_list()[i]
)[0]["id"]
else:
temp_list_of_genres_per_movie[i] = 0
movies_metadata["genres"] = temp_list_of_genres_per_movie
return movies_metadata
def process_spoken_languages(movies_metadata):
list_of_spoken_languages_per_movie = movies_metadata["spoken_languages"].to_list()
temp_list_of_spoken_languages_per_movie = []
temp_list_of_spoken_languages_per_movie.extend(list_of_spoken_languages_per_movie)
amount_of_spoken_languages_per_movie = [0] * len(
movies_metadata["spoken_languages"]
)
for i in range(len(movies_metadata["spoken_languages"])):
if temp_list_of_spoken_languages_per_movie[i] != "[]":
amount_of_spoken_languages_per_movie[i] = len(
ast.literal_eval(list_of_spoken_languages_per_movie[i])
)
else:
amount_of_spoken_languages_per_movie[i] = 0
movies_metadata["spoken_languages"] = amount_of_spoken_languages_per_movie
return movies_metadata
def process_production_countries(movies_metadata):
list_of_production_countries_per_movie = movies_metadata[
"production_countries"
].to_list()
temp_list_of_production_countries_per_movie = []
temp_list_of_production_countries_per_movie.extend(
list_of_production_countries_per_movie
)
for i in range(len(movies_metadata["production_countries"])):
if (
temp_list_of_production_countries_per_movie[i] != "[]"
and not temp_list_of_production_countries_per_movie[i] in COUNTRY_CODES
):
temp_list_of_production_countries_per_movie[i] = COUNTRY_CODES.index(
(
ast.literal_eval(list_of_production_countries_per_movie[i])[0][
"iso_3166_1"
]
).lower()
)
else:
temp_list_of_production_countries_per_movie[i] = 0
movies_metadata["production_countries"] = (
temp_list_of_production_countries_per_movie
)
return movies_metadata
def process_release_date(movies_metadata):
list_of_release_date_per_movie = movies_metadata["release_date"].to_list()
temp_list_of_release_date_per_movie = []
temp_list_of_release_date_per_movie.extend(list_of_release_date_per_movie)
for i in range(len(movies_metadata["release_date"])):
if temp_list_of_release_date_per_movie[i] != "" and not pd.isnull(
temp_list_of_release_date_per_movie[i]
):
temp_list_of_release_date_per_movie[i] = (
temp_list_of_release_date_per_movie[i].replace("-", "")[:-2]
)
else:
temp_list_of_release_date_per_movie[i] = 0
movies_metadata["release_date"] = temp_list_of_release_date_per_movie
return movies_metadata
def process_original_language(movies_metadata):
list_of_original_language_per_movie = movies_metadata["original_language"].to_list()
for i in range(len(movies_metadata["original_language"])):
if list_of_original_language_per_movie[i] != "[]" and not pd.isnull(
list_of_original_language_per_movie[i]
):
list_of_original_language_per_movie[i] = COUNTRY_CODES.index(
list_of_original_language_per_movie[i].lower()
)
else:
list_of_original_language_per_movie[i] = 0
movies_metadata["original_language"] = list_of_original_language_per_movie
return movies_metadata
def process_cast_director(
movies_metadata: pd.DataFrame, credits_df: pd.DataFrame
) -> list:
top_actors_list: list[int] = []
directors_list: list[int] = []
num_of_misses_cast = 0
num_of_misses_director = 0
for entry in movies_metadata.itertuples():
tmp = credits_df[credits_df["id"] == int(entry.id)]
if len(tmp.cast.to_list()) != 0:
cast = ast.literal_eval(tmp.cast.to_list()[0])
else:
cast = []
if len(tmp.crew.to_list()) != 0:
crew = ast.literal_eval(tmp.crew.to_list()[0])
else:
crew = []
actor_id = cast[0]["id"] if len(cast) > 0 else -1
director_id = -1
for i in range(len(cast)):
if "job" in cast[i]:
if cast[i]["job"] == "Director":
director_id = cast[i]["id"]
break
if director_id == -1:
for i in range(len(crew)):
if crew[i]["job"] == "Director":
director_id = crew[i]["id"]
break
if actor_id == 1:
num_of_misses_cast += 1
if director_id == 1:
num_of_misses_director += 1
top_actors_list.append(actor_id)
directors_list.append(director_id)
print(f"Cast misses: {num_of_misses_cast}")
print(f"Director misses: {num_of_misses_director}")
movies_metadata["top_actor_id"] = top_actors_list
movies_metadata["director_id"] = directors_list
return movies_metadata
def _process_keywords(movies_metadata: pd.DataFrame, keywords_df: pd.DataFrame):
list_keywords_per_movie = []
num_of_misses = 0
for entry in keywords_df.itertuples():
if len(entry.keywords) != 0:
keywords = ast.literal_eval(entry.keywords)
tmdbId = entry.id
if len(keywords) > 0:
temp = {"tmdbId": tmdbId, "keyword": keywords[0]["id"]}
list_keywords_per_movie.append(temp)
continue
num_of_misses += 1
movies_metadata["keyword_id"] = list_keywords_per_movie
return movies_metadata
credits_file_path = r"input/archive/credits.csv"
credits_df = pd.read_csv(credits_file_path)
keywords_file_path = r"input/archive/keywords.csv"
keywords_df = pd.read_csv(keywords_file_path)
file_path = r"output/avg_of_rating_per_movieId.csv"
movies_df = pd.read_csv(file_path)
movies_metadata_file_path = r"input/archive/movies_metadata.csv"
movies_metadata = pd.read_csv(movies_metadata_file_path, low_memory=False)
movie_ids = movies_df["movieId"].to_list()
movie_ids2 = movies_metadata["id"].to_list()
movies_metadata = process_cast_director(movies_metadata, credits_df)
print("Director and top actor DONE")
movies_metadata = process_genres(movies_metadata)
print("Genres DONE")
movies_metadata = process_spoken_languages(movies_metadata)
print("Spoken languages DONE")
movies_metadata = process_production_countries(movies_metadata)
print("Production countries DONE")
movies_metadata = process_release_date(movies_metadata)
print("Release date DONE")
movies_metadata = process_original_language(movies_metadata)
print("Original language DONE")
print("Maching ids metadata and ratings...")
ids = movies_metadata["imdb_id"].to_list()
ids2 = movies_df["imdbId"].to_list()
ratings = movies_df["avg_of_rating"].to_list()
result = []
resultIds = []
for i in range(len(ids)):
found = False
for j in range(len(ids2)):
if ids[i] == ids2[j]:
found = True
result.append(ratings[j])
resultIds.append(ids2[j])
break
if found == False:
result.append(0)
resultIds.append(ids[i])
new_avg = {"imdbId": resultIds, "avg_of_rating": result}
print("Maching ids metadata and ratings DONE")
output_data = {
"matched_ids_avg": new_avg["imdbId"],
"movieId_movies_metadata": movies_metadata["imdb_id"],
"avg_of_rating": new_avg["avg_of_rating"],
"budget": movies_metadata["budget"],
"director_id": movies_metadata["director_id"],
"top_actor_id": movies_metadata["top_actor_id"],
"genres": movies_metadata["genres"],
"original_language": movies_metadata["original_language"],
"release_date": movies_metadata["release_date"],
"revenue": movies_metadata["revenue"],
"spoken_languages": movies_metadata["spoken_languages"],
"runtime": movies_metadata["runtime"],
"production_countries": movies_metadata["production_countries"],
"vote_count": movies_metadata["vote_count"],
}
output_file = pd.DataFrame(output_data)
output_file = output_file[output_file["budget"] != 0]
output_file = output_file[output_file["genres"] != 0]
output_file = output_file[output_file["original_language"] != 0]
output_file = output_file[output_file["release_date"] != 0]
output_file = output_file[output_file["revenue"] != 0]
output_file = output_file[output_file["spoken_languages"] != 0]
W ten sposób uzyskaliśmy dane uczące, ponad 5 tyś rekordów, które możemy wykorzystać do treningu modelów na podstawie cech:
- Średnia ocena użytkowników
- Budżet
- Reżyser
- Aktor grający główną rolę
- Gatunek
- Orginalny język
- Data premiery
- Przychód
- Ilość dubbingów
- Czas trwania
- Główny kraj produkcji
- Liczba głosów
Konwersja typów zmiennych¶
Dane z postaci tekstowej zostały przekonwertowane do podpowiednich fomatów:
- Budżet - liczba całkowita
- Reżyser - unikalne id (liczba całkowita)
- Główny aktor - unikalne id (liczba całkowita)
- Gatunek - unikalne id (liczba całkowita)
- Orginalny język - unikalne id (liczba całkowita)
- Data premiery - liczba całkowita rok miesiąc np. 202201
- Przychód - liczba całkowita
- Ilość dubbingów - liczba całkowita
- Czas trwania (w sekundach) - liczba całkowita
- Główny kraj produkcji - unikalne id (liczba całkowita)
- Liczba głosów - liczba całkowita
Średnia ocena użytkowników¶
To wartość liczbowa reprezentująca przeciętną ocenę przyznaną filmowi przez użytkowników. Skala wynosi od 0 do 5. Wysoka średnia ocena zazwyczaj przekłada się na pozytywne przyjęcie filmu przez publiczność.
Reżyser¶
Reżyser jest znaczącą rolą w procesie tworzenia filmu. Znane osoby jak Quentin Tarantino, Tim Burton, czy Christopher Nolan potrafią przyciągnąć samym swoim nazwiskiem do sal kinowych. Zarówno sympatia ich fanów, jak i zdolności reżyserskie wpływają pozytywnie na odbiór filmu.
Aktor grający główną rolę¶
Aktor, bądź aktorka grający główną rolę są odpowiedzialni za wizerunek filmu, to ich twarze będą widoczne na plakatach promujących. Aktorzy są nawet bardziej znani niż reżyserzy. Ich status celebryty potrafi przyciągać tłumy.
Budżet¶
Całkowity koszt produkcji filmu. Zakres wartości: od setek do setek milionów dolarów. Budżet potrafi znacząco wpłynąć na sukces wśród widowni, choć wiele klasycznych produkcji pokazuje, że i z niskim budżetem można wywrzeć wrażenie na publiczności.
Gatunek¶
Kategoria tematyczna filmu (np. komedia, dramat, horror, science fiction). Zakres wartości: id głównego gatunek filmu. Można się spodziewać, że pewne gatunki przyciągają większą widownię.
Język oryginału¶
Język, w którym film został pierwotnie nagrany i wyprodukowany. Zakres wartości: id języka będące liczbą całkowitą dodatnią. Filmy, których oryginalny język jest popularny na świecie jak angielski, chiński czy hiszpański zazwyczaj trafiają do większej ilości odbiorców.
Data premiery¶
Data, kiedy film został po raz pierwszy wyświetlony publicznie. Zakres wartości: format daty (YYYYMM). Filmy z pewnych okresów cieszą się lepszą opinią.
Przychód¶
Całkowity przychód wygenerowany przez film. Zakres wartości: od kilku do miliardów dolarów. Przychód jest wskaźnikiem komercyjnego sukcesu filmu, zazwyczaj przekłada się na ocenę filmu.
Ilość dubbingów¶
Liczba języków, na które film został przetłumaczony. Zakres wartości: może wynosić od 0 do kilkudziesięciu. Dubbing pozostaje jedną z najbardziej popularnych form przygotowania filmu do odbioru przez obcokrajowców.
Czas trwania¶
Całkowity czas trwania filmu, podany w minutach. Zakres wartości: od około 20-minutowych do nawet ponad 200 minut. Czas trwania bezpośrednio ma wpływ na ocenę. Filmy zbyt długie stają się nużące, a krótkie mogą nie zaspokoić potrzeby widza.
Kraj produkcji¶
Id kraju (liczba całkowita), będącego głównym krajem produkcji
Liczba głosów¶
Całkowita liczba ocen oddanych przez użytkowników. Zakres wartości: może wynosić od kilku do milionów. Większa liczba głosów przekłada się na popularność i wiarygodność średniej oceny filmu.
Wizualizacja danych¶
Do wizualizacji danych, używamy biblioteki pyplot i seaborn.
Importowanie bibliotek i danych z pliku .csv¶
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from constants import NUMERICAL_COLUMNS
file_path = "output/movies_relevant_data_num_ids.csv"
movies_df = pd.read_csv(file_path)
movies_df['release_date'] = movies_df['release_date'] / 100
numerical_columns = [
"avg_of_rating",
#"director_id",
# "top_actor_id",
"budget",
# "genres",
"original_language",
"release_date",
"revenue",
#"spoken_languages",
"runtime",
"production_countries",
"vote_count",
]
# polskie nazwy kolumn
polish_column_names = [
"średnich ocen",
"budżetu",
"język oryginalnego",
"daty premiery",
"dochodu",
"czasu trwania",
"krajów produkcji",
"liczby ocen",
]
Rozkład cech¶
plt.figure(figsize=(15, 10))
for i, col in enumerate(numerical_columns, 1):
plt.subplot(3, 4, i)
sns.histplot(movies_df[col])
plt.title(f'Rozkład {polish_column_names[i-1]}')
plt.xlabel(col)
plt.ylabel('częstotliwość')
plt.tight_layout()
plt.show()
Średnia ocena¶
Częstotliwość średnich posiada rozkład normalny, przesunięty nieznacznie w prawą stronę osi X. Jak większość zjawisk w przyrodzie w społeczeństwie najczęściej spotykane są filmy przeciętne. Z oceną około 3. Przesuniecie, prawdopodobnie wynika z niechęci użytkowników do zostawiania najniższych ocen.
Rozkład budżetu¶
Znacząca większość filmów posiada budżet poniżej 100 milionów dolarów, najczęściej występujący, to około miliona dolarów. Nieliczne produkcje posiadają budżet miniaturowy budżet, nawet 250 dolarów.
Rozkład częstotliwości języka orginalnego¶
Najczęściej spotykanym ID języka jest to należące do angielskiego. Nie jest to zaskoczeniem, jako że jest to język międzynarodowy.
Rozkład roku premiery¶
Zauważyć na tym histogramie można tendencję wzrostową, wraz z biegiem lat coraz więcej filmów jest produkowanych. W dodatku premiera nowego dzieła potencjalnie przyciąga uwagę klientów.
Rozkład dochodu¶
Dochód zależy od rodzaju filmu, pewne z nich tzw. blockbustery są tworzone z myślą o dochodzie. Kino artystyczne lub filmy ze słabym marketingiem, czy wypuszczone w mało dochodowych formach jak np. tylko DVD zarobią mniej niż takie dostępne w kinach. Większość filmów blockbusterami nie jest i ich dochód jest mniejszy od 30 milionów dolarów.
Rozkład czasu trwania¶
Najczęściej pojawiającą się wartością jest bez niespodzianki 90 minut, co jest popularną wartością dla większości filmów pełnometrażowych, animacji etc.
Kraj produkcji¶
Od kraju produkcji zależą studia, które mogły produkować film i sceneria.
Rozkład ilości głosów na osobę¶
Histogram jednoznacznie pokazuje, że większość użytkowników zostawia od kilku do kilkunastu ocen.
Tendencja przychodu na przestrzeni lat¶
plt.figure(figsize=(12, 6))
sns.lineplot(x='release_date', y='revenue', data=movies_df)
plt.title('Tendencja przychodu na przestrzeni lat')
plt.xlabel('Rok')
plt.ylabel('Przychód')
plt.show()
Przychód ze wszystkich filmów na przestrzeni lat rósł, z okresowymi nagłymi i znaczącymi skokami w poszczególnych latach. Najbardziej jest to dostrzegalne w późnych latach 70. Warto pamiętać, że nieuwzględniona zostaje tu inflacja.
Tendencja budżetu na przestrzeni lat¶
# # Plotting the trend of budget over the years
plt.figure(figsize=(12, 6))
sns.lineplot(x='release_date', y='budget', data=movies_df)
plt.title('Tendencja budżetu na przestrzeni lat')
plt.xlabel('Rok')
plt.ylabel('Budżet')
plt.show()
Z biegiem czasu budżet stopniowo wzrasta. Za to potencjalnie może być odpowiedzialne wiele czynników, od wzrastających wypłat aktorów, reżyserów, ekip od efektów specjalnych, muzyki etc; po inflację i rosnące koszta technologii filmowych.
Relacja między budżetem, a przychodem¶
plt.figure(figsize=(12, 6))
sns.scatterplot(x='budget', y='revenue', data=movies_df)
plt.title('Relacja między budżetem, a przychodem')
plt.xlabel('Budżet')
plt.ylabel('Przychód')
plt.show()
Powyższy wykres przedstawią relacje budżetu i przychodu. O ile najbardziej dochodowe filmy posiadają wysoki budżet, to wiele filmów o niskim budżecie byo bardziej oplacalnych bo przynioslo większy dochód niż w nie zainwestowano
Relacja między budżetem, a średnią oceną¶
plt.figure(figsize=(12, 6))
sns.scatterplot(x='avg_of_rating', y='budget', data=movies_df)
plt.title('Relacja między budżetem, a średnią oceną')
plt.xlabel('Średnia ocena')
plt.ylabel('Budżet')
plt.show()
Można zauważyć, że w przeciwieństwie do relacji między budżetem, a dochodem to wyższy budżet zapewnia ocenę, która w najgorszym wypadku będzie zbliżona do przeciętniej. Filmy o niskim budżecie mają większy rozrzut ocenowy, niz te z wysokim. Spodziewać się można, że bardziej będą na s
Statystyki opisowe¶
Wykorzystująć wbudowane metody do klasy dataFrame z biblioteki pandas, obliczyliśmy statystyki opisowe i przedstawiliśmy w formie tabeli:
data = [[] for _ in range(len(numerical_columns))]
for i, col in enumerate(numerical_columns, 1):
data[i-1].append(col)
data[i-1].append(round(movies_df[col].mean(), 2))
data[i-1].append(round(movies_df[col].median(), 2))
data[i-1].append(round(movies_df[col].min(), 2))
data[i-1].append(movies_df[col].max())
data[i-1].append(round(movies_df[col].std(), 2))
data[i-1].append(round(movies_df[col].var(), 2))
data[i-1].append(round(movies_df[col].sum(), 2))
data[i-1].append(round(movies_df[col].count(), 2))
data[i-1].append(round(movies_df[col].quantile(0.25), 2))
data[i-1].append(round(movies_df[col].skew(), 2))
# tworzenie DataFrame z danymi statystycznymi i polskimi nazwami kolumn
pd.DataFrame(data, columns=["Kolumna", "Średnia", "Mediana", "Minimum", "Maksimum", "Odchylenie standardowe", "Wariancja", "Suma", "Liczba wartości", "Kwartyl 1", "Skośność"])
| Kolumna | Średnia | Mediana | Minimum | Maksimum | Odchylenie standardowe | Wariancja | Suma | Liczba wartości | Kwartyl 1 | Skośność | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | avg_of_rating | 3.200 | 3.260 | 0.000 | 5.000 | 0.530 | 0.280 | 16,906.350 | 5283 | 2.880 | -0.850 |
| 1 | budget | 31,552,083.180 | 17,000,000.000 | 1.000 | 380,000,000.000 | 40,359,873.340 | 1,628,919,375,972,667.250 | 166,689,655,414.000 | 5283 | 5,830,000.000 | 2.500 |
| 2 | original_language | 97.040 | 100.000 | 4.000 | 201.000 | 24.510 | 600.770 | 512,684.000 | 5283 | 100.000 | -0.930 |
| 3 | release_date | 199,990.080 | 200,411.000 | 191,502.000 | 201,708.000 | 1,575.790 | 2,483,120.030 | 1,056,547,574.000 | 5283 | 199,407.000 | -1.830 |
| 4 | revenue | 91,866,701.870 | 30,859,000.000 | 1.000 | 2,787,965,087.000 | 167,267,079.590 | 27,978,275,912,926,536.000 | 485,331,785,996.000 | 5283 | 7,637,468.500 | 4.440 |
| 5 | runtime | 110.150 | 106.000 | 26.000 | 338.000 | 21.510 | 462.640 | 581,902.000 | 5283 | 95.000 | 1.640 |
| 6 | production_countries | 52.720 | 27.000 | 2.000 | 204.000 | 53.290 | 2,839.350 | 278,504.000 | 5283 | 27.000 | 1.690 |
| 7 | vote_count | 745.430 | 286.000 | 1.000 | 14,075.000 | 1,259.270 | 1,585,763.320 | 3,938,107.000 | 5283 | 84.000 | 3.700 |
Braki danych¶
Spotkaliśmy się z wieloma brakami danych, przede wszystkim w kolumnie revenue i budget (przychód i budżet). Aby stworzony przez nas model był jak najbardziej dokładny, owe rekordy z brakami zostały usunięte. Posiadana ilość danych jest na tyle wysoka, że nie powinno to zmienić jego efektywności.
Obserwacje odstające¶
Ze względu na specyfikę i różnorodność produkcji filmowych, postanowiliśmy nie usuwać odstających danych, nie zaszła taka potrzeba.
Klasteryzacja¶
Dane dodatkowo podzieliliśmy na grupy, przy pomocy klasteryzacji metodą k-n średnich:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.cluster import KMeans
from constants import NUMERICAL_COLUMNS
file_path = "output/movies_relevant_data_num_ids.csv"
movies_df = pd.read_csv(file_path)
clustering_features_simple = NUMERICAL_COLUMNS
X_clustering_simple = movies_df[clustering_features_simple]
wcss = []
for i in range(1, 11):
kmeans = KMeans(n_clusters=i, init="k-means++", random_state=42)
kmeans.fit(X_clustering_simple)
wcss.append(kmeans.inertia_)
second_derivative = (
[0] + [wcss[i] - 2 * wcss[i + 1] + wcss[i + 2] for i in range(len(wcss) - 2)] + [0]
)
optimal_k = second_derivative.index(max(second_derivative)) + 1
print(f"Optymalna ilość grup: {optimal_k}")
plt.figure(figsize=(10, 6))
plt.plot(range(1, 11), wcss, marker="o")
plt.title("Metoda łokciowa")
plt.xlabel("Liczba skupień")
plt.ylabel("WCSS")
plt.show()
# optimal_k = 2
Optymalna ilość grup: 2
Najoptymalniejsza ilością jest dwa, jako że druga pochodna przyjmuję tam największą wartość, widoczne jest to także w polu pod wykresem. Przyrost pola dla liczby jest stosunkowo już znacznie mniejszy.
Przy pomocy metody k-średnich dane zostały podzielone na dwie grupy. Wykresy każdej pary zmiennych zamieszczone są poniżej, bez kodu generejuącego je ze względu na czas trwania wykonywania programu tworzącego je (lin_reg_clustering.py).
from IPython.display import Image
Image(filename="output/kmeans_pairplot.png")
Wykorzystane metody badawcze¶
Do implementacji metod wykorzystaliśmy bibliotękę sciklearn, która zawiera już przygotowane klasy wykonujące pożądane przez nas metody:
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import (
accuracy_score,
classification_report,
confusion_matrix,
)
from sklearn.linear_model import LogisticRegression
Ładowanie danych. Dokonujemy podziału na dwie grupy według mediany ocen wszystkich filmów. Grupa pierwsza to filmy złe (wartość 0), a druga to filmy dobre (wartość 1).
data = pd.read_csv("output/movies_relevant_data_num_ids.csv")
threshold = data["avg_of_rating"].median()
data["label"] = (data["avg_of_rating"] >= threshold).astype(int)
features = [
"avg_of_rating",
"director_id",
"top_actor_id",
"budget",
"genres",
"original_language",
"release_date",
"revenue",
"spoken_languages",
"runtime",
"production_countries",
"vote_count",
]
X = data[features].drop("avg_of_rating", axis=1)
y = data["label"]
Używamy metody klasy StandardScaler, by dokonać standaryzacji danych wejściowych. Bez tego klasyfikacja byłaby niemożliwa.
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X)
Tworzenie grupy testowej i trenującej:
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
Funkcja zwracająca macierz błędu dla danej metody:
import numpy as np
def plot_confusion_matrix(y_test, y_pred, method_name=''):
labels = np.array(y_test)
preds = np.array(y_pred)
cm = confusion_matrix(labels, preds)
plt.figure(figsize=(10, 7))
sns.heatmap(
cm,
annot=cm,
fmt="d",
xticklabels=["Przewidziany zły", "Przewidziany dobry"],
yticklabels=["Faktycznie zły", "Faktycznie dobry"],
)
plt.xlabel("Przewidziany stan")
plt.ylabel("Stan faktyczny")
plt.title("Macierz błędu")
file_name = r"output/confusion_matrixes/confusion_matrix_" + method_name + ".png"
plt.savefig(file_name)
plt.show()
1. Regresja logistyczna¶
Regresja logistyczna to metoda statystyczna stosowana do przewidywania prawdopodobieństwa wystąpienia zdarzenia binarnego. Jest to powszechnie stosowana technika w uczeniu maszynowym ze względu na jej prostotę i interpretowalność.
- Źródło: Hosmer, Fred A. Jr., Stanley Lemeshow. (2004). A Primer on Logistic Regression. Journal of the National Cancer Institute 96, nr 2: 102-107. https://pubmed.ncbi.nlm.nih.gov/34952854
Funkcja logistyczna (sigmoid)¶
Regresja logistyczna do określenie wartości przewidywanej klasy wykorzystuje funkcję logistyczną, zwana również sigmoidalną. Przekształca wynik liniowej kombinacji cech na prawdopodobieństwo:
$ \large \sigma(z) = \frac{1}{1 + e^{-z}} $
Gdzie:
- $ z $ jest liniową kombinacją cech, $ z = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \ldots + \beta_n x_n $
- $ \beta_0, \beta_1, \ldots, \beta_n $ są cechami modelu
Funkcja kosztu¶
Regresja logistyczna minimalizuje funkcję kosztu zwaną log-loss lub cross-entropy:
$ J(\beta) = -\frac{1}{m} \sum_{i=1}^{m} \left[ y^{(i)} \log(h_\beta(x^{(i)})) + (1 - y^{(i)}) \log(1 - h_\beta(x^{(i)})) \right] $
Gdzie:
- $ m $ jest liczbą próbek w zbiorze treningowym
- $ y^{(i)} $ jest rzeczywistą etykietą dla próbki $ i $
- $ h_\beta(x^{(i)}) $ jest przewidywanym prawdopodobieństwem dla próbki $ i $
Przewidywanie¶
Aby sklasyfikować nową próbkę, regresja logistyczna używa następującej reguły decyzyjnej:
$ h_\beta(x) \geq 0.5 \implies \text{Klasa 1} $
$ h_\beta(x) < 0.5 \implies \text{Klasa 0} $
from sklearn.linear_model import LogisticRegression
import numpy as np
import seaborn as sns
logistic = LogisticRegression(max_iter=10000)
logistic.fit(X_train, y_train)
y_pred = logistic.predict(X_test)
print("Logistic Regression")
print(f"Dokładność {accuracy_score(y_test, y_pred):.5f}")
log_reg_class_rap = classification_report(y_test, y_pred)
Logistic Regression Dokładność 0.73132
plot_confusion_matrix(y_test, y_pred, "logistic regression")
2. Model K najbliższych sąsiadów (KNN)¶
Jest to algorytm, który klasyfikuje nowe punkty danych na podstawie ich podobieństwa do istniejących punktów danych w zbiorze treningowym.
- Źródło: Mohseni, Mehrzad, Payam Ghahramani, Richard E. Fremouw. (2007). K-Nearest Neighbors for Multi-Label Classification. Proceedings of the 24th International Conference on Machine Learning. 1-2. https://www.jmlr.org/papers/volume25/23-0286/23-0286.pdf.
Kroki algorytmu k-NN¶
- Wybór liczby k - najbliższych sąsiadów
- Obliczenie odległości euklidesową między nowym przypadkiem a wszystkimi innymi przypadkami w zbiorze danych
- Wybór k sąsiadów najmniejszą odległość do nowego przypadku
- Głosowanie większościowe większościowej klasy wśród k najbliższych sąsiadów
Odległość euklidesowa¶
Odległość euklidesowa jest najczęściej używaną miarą odległości w algorytmie k-NN. Wzór na odległość euklidesową między dwoma punktami $A = (a_1, a_2, \ldots, a_n)$ i $B = (b_1, b_2, \ldots, b_n)$ w n-wymiarowej przestrzeni wygląda następująco:
$ d(A, B) = \sqrt{(a_1 - b_1)^2 + (a_2 - b_2)^2 + \ldots + (a_n - b_n)^2} $
Implementacja w kodzie¶
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier(n_neighbors=31)
knn.fit(X_train, y_train)
y_pred = knn.predict(X_test)
print("KNN")
print(f"Dokładność {accuracy_score(y_test, y_pred):.5f}")
knn_class_rap = classification_report(y_test, y_pred)
KNN Dokładność 0.71050
plot_confusion_matrix(y_test, y_pred, 'knn')
Algorytm znajdowania najlepszej wartości parametru:
def knn_best_params(X_train, y_train, X_test, y_test):
best_k = 0
best_score = 0
for k in range(1, 100):
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train, y_train)
y_pred = knn.predict(X_test)
score = accuracy_score(y_test, y_pred)
if score > best_score:
best_score = score
best_k = k
return best_k, best_score
3. Model SVC (Support Vector Classification)¶
Jest to metoda, która znajduje granice decyzyjne między klasami danych poprzez identyfikację wektorów podporowych.
- Źródło: Joachim, Thorsten. (2011). Support Vector Machines. Applied Mathematics and Optimization 62, nr. 2: 443-474. https://link.springer.com/chapter/10.1007/0-387-25465-X_12.
Założenia SVC¶
- SVC znajduje hiperpłaszczyznę, która maksymalizuje margines. Margines to odległość między najbliższymi punktami danych obu klas a hiperpłaszczyzną.
- Maksymalizacja Marginesu Prowadzi to do lepszej generalizacji modelu na nowe dane.
- SVC może stosować różne funkcje jądra (kernels), takie jak liniowe, wielomianowe, radialne (RBF) itp., aby przekształcić dane wejściowe na wyższą przestrzeń wymiarową, gdzie łatwiej jest znaleźć rozdzielającą hiperpłaszczyzą. Użyta w projekcie funkcja jądra jest liniowa.
Funkcja decyzyjna¶
Funkcja decyzyjna w SVC ma postać:
$ f(x) = \text{sign}\left(\sum_{i=1}^{n} \alpha_i y_i K(x_i, x) + b\right) $
Gdzie:
- $ \alpha_i $ to współczynniki Lagrange'a, które są wyznaczane w procesie trenowania
- $ y_i $ to etykieta klasy dla punktu $ x_i $
- $ K(x_i, x) $ to funkcja jądra, która oblicza podobieństwo między punktami $ x_i $ i $ x $
- $ b $ to stała wyznaczona w procesie trenowania
from sklearn.svm import LinearSVC
svc = LinearSVC(dual=False, max_iter=10000)
svc.fit(X_train, y_train)
y_pred = svc.predict(X_test)
print("Linear SVC")
print(f"Dokładność {accuracy_score(y_test, y_pred):.5f}")
lin_svc_class_rap = classification_report(y_test, y_pred)
Linear SVC Dokładność 0.73132
plot_confusion_matrix(y_test, y_pred, 'linearSVC')
4. Metoda hybrydowa¶
Przewiduje etykiety klas dla podanych wartości wejściowych za pomocą wymyślonej przez nas metody. Wykorzystuje następujące klasyfikatory:
- LinearSVC
- Regresję Logistyczną
- KNeighborsClassifier
Końcowa predykcja jest średnią arytmetyczną z predykcji trzech klasyfikatorów.
def hybrid_method_predict(X_vals, X_train) -> np.ndarray:
svc = LinearSVC(dual=True, max_iter=10000)
svc.fit(X_train, y_train)
d1 = svc.predict(X_vals)
logistic = LogisticRegression(max_iter=10000)
logistic.fit(X_train, y_train)
d2 = logistic.predict(X_vals)
knn = KNeighborsClassifier(n_neighbors=31)
knn.fit(X_train, y_train)
d3 = knn.predict(X_vals)
ans = []
for val1, val2, val3 in zip(d1, d2, d3):
num_of_zeros = 0
num_of_ones = 0
for val in [val1, val2, val3]:
if val == 0:
num_of_zeros += 1
else:
num_of_ones += 1
ans.append(1 if num_of_zeros <= num_of_ones else 0)
return np.array(ans)
y_pred = hybrid_method_predict(X_test, X_train)
print("Hybrid Method")
print(f"Dokładność {accuracy_score(y_test, y_pred):.5f}")
hybrid_class_rap = classification_report(y_test, y_pred)
Hybrid Method Dokładność 0.73226
plot_confusion_matrix(y_test, y_pred, 'hybrid')
Raporty klasyfikcaji¶
print("K najbliższych sąsiadów:")
print(knn_class_rap)
print("Regresja logistyczna:")
print(log_reg_class_rap)
print("SVC:")
print(lin_svc_class_rap)
print("Hybrydowa metoda:")
print(hybrid_class_rap)
K najbliższych sąsiadów:
precision recall f1-score support
0 0.69 0.76 0.72 525
1 0.74 0.66 0.70 532
accuracy 0.71 1057
macro avg 0.71 0.71 0.71 1057
weighted avg 0.71 0.71 0.71 1057
Regresja logistyczna:
precision recall f1-score support
0 0.70 0.81 0.75 525
1 0.77 0.66 0.71 532
accuracy 0.73 1057
macro avg 0.74 0.73 0.73 1057
weighted avg 0.74 0.73 0.73 1057
SVC:
precision recall f1-score support
0 0.69 0.82 0.75 525
1 0.79 0.64 0.71 532
accuracy 0.73 1057
macro avg 0.74 0.73 0.73 1057
weighted avg 0.74 0.73 0.73 1057
Hybrydowa metoda:
precision recall f1-score support
0 0.70 0.82 0.75 525
1 0.78 0.65 0.71 532
accuracy 0.73 1057
macro avg 0.74 0.73 0.73 1057
weighted avg 0.74 0.73 0.73 1057
Dokładność (accuracy)¶
Dokładność (Accuracy) jest miarą jakości klasyfikatora, określającą stosunek liczby poprawnie przewidzianych przypadków (zarówno pozytywnych, jak i negatywnych) do liczby wszystkich przypadków. Wzór na dokładność wygląda następująco:
$ \text{Dokładność} = \frac{TP + TN}{TP + TN + FP + FN} $
Gdzie:
- $ TP $ (True Positives) - liczba prawdziwych pozytywów (przypadki poprawnie sklasyfikowane jako pozytywne)
- $ TN $ (True Negatives) - liczba prawdziwych negatywów (przypadki poprawnie sklasyfikowane jako negatywne)
- $ FP $ (False Positives) - liczba fałszywych pozytywów (przypadki niepoprawnie sklasyfikowane jako pozytywne)
- $ FN $ (False Negatives) - liczba fałszywych negatywów (przypadki niepoprawnie sklasyfikowane jako negatywne)
Precyzja (precision)¶
Precyzja (precision) jest miarą jakości klasyfikatora, określającą stosunek liczby poprawnie przewidzianych pozytywnych przypadków do liczby wszystkich przypadków przewidzianych jako pozytywne.
$\text{precision} = \frac{TP}{TP + FP}$
Gdzie:
- $ TP $ (True Positives) - liczba prawdziwych pozytywów (przypadki poprawnie sklasyfikowane jako pozytywne)
- $ FP $ (False Positives) - liczba fałszywych pozytywów (przypadki niepoprawnie sklasyfikowane jako pozytywne)
Czułość¶
Czułość (Recall) jest miarą jakości klasyfikatora, określającą stosunek liczby poprawnie przewidzianych pozytywnych przypadków do liczby wszystkich rzeczywistych pozytywnych przypadków. Wzór na czułość wygląda następująco:
$ \text{Czułość} = \frac{TP}{TP + FN} $
Gdzie:
- $ TP $ (True Positives) - liczba prawdziwych pozytywów (przypadki poprawnie sklasyfikowane jako pozytywne)
- $ FN $ (False Negatives) - liczba fałszywych negatywów (przypadki niepoprawnie sklasyfikowane jako negatywne)
Miara F-1¶
Miara F-1 (z ang. F1-score) jest miarą jakości klasyfikatora, która łączy precyzję (Precision) i czułość (Recall) w jedną wartość. Jest to średnia harmoniczna precyzji i czułości.
$ \text{F1-Score} = 2 \cdot \frac{\text{Precyzja} \cdot \text{Czułość}}{\text{Precyzja} + \text{Czułość}} $
Gdzie:
- $ \text{Precyzja} = \frac{TP}{TP + FP} $
- $ \text{Czułość} \, (\text{Recall}) = \frac{TP}{TP + FN} $
Gdzie:
- $ TP $ (True Positives) - liczba prawdziwych pozytywów (przypadki poprawnie sklasyfikowane jako pozytywne)
- $ FP $ (False Positives) - liczba fałszywych pozytywów (przypadki niepoprawnie sklasyfikowane jako pozytywne)
- $ FN $ (False Negatives) - liczba fałszywych negatywów (przypadki niepoprawnie sklasyfikowane jako negatywne)
Walidacja krzyżowa¶
Walidacja krzyżowa (cross-validation) to technika oceny modeli uczenia maszynowego, która polega na podzieleniu dostępnych danych na zestawy treningowe i testowe w celu uzyskania bardziej niezawodnej miary wydajności modelu. Umożliwia to ocenę, jak model będzie się sprawdzał na nowych, niewidzianych wcześniej danych.
from sklearn.model_selection import cross_val_score
scores = cross_val_score(knn, X, y, cv=10)
score_knn = cross_val_score(knn, X, y, cv=10)
score_svc = cross_val_score(svc, X, y, cv=10)
score_logistic = cross_val_score(logistic, X, y, cv=10)
# Wyświetlenie wyników
print(f"Średni wynik walidacji krzyżowej: {scores.mean():.2f}")
print(f"Odchylenie standardowe wyników: {scores.std():.2f}")
Średni wynik walidacji krzyżowej: 0.70 Odchylenie standardowe wyników: 0.05
Testowanie modelu na sztucznie wytworzonych obserwacjach¶
pd.options.display.float_format = '{:,.3f}'.format
def generate_random_values(num_samples=10):
df = pd.read_csv("./output/movies_relevant_data_num_ids.csv")
df.drop("avg_of_rating", axis=1, inplace=True) # Drop the target column
df.drop(
"movieId_movies_metadata", axis=1, inplace=True
) # Drop the movieId column (it's not a feature
random_movies = []
columns = df.columns
for _ in range(num_samples):
movie = {}
for column in columns:
# Use its value for the current column
i = np.random.randint(0, len(df[column]) - 1)
movie[column] = df[column][i]
random_movies.append(movie)
return pd.DataFrame(random_movies)
gen_columns = features
gen_columns.remove('avg_of_rating')
random_movies_df = generate_random_values(num_samples=10)
pd.DataFrame(random_movies_df, columns=gen_columns)
| director_id | top_actor_id | budget | genres | original_language | release_date | revenue | spoken_languages | runtime | production_countries | vote_count | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 64823 | 134 | 70000000 | 10749 | 100 | 200106 | 3000000 | 1 | 135.000 | 27 | 3359 |
| 1 | 4945 | 1230 | 20000000 | 28 | 100 | 200205 | 38629478 | 1 | 128.000 | 27 | 144 |
| 2 | 27005 | 2955 | 1300000 | 18 | 100 | 195204 | 112462508 | 1 | 82.000 | 27 | 25 |
| 3 | 17494 | 36801 | 25000000 | 35 | 100 | 201006 | 3324330 | 1 | 89.000 | 158 | 1837 |
| 4 | 1032 | 51329 | 90000000 | 18 | 100 | 201110 | 36144000 | 1 | 94.000 | 27 | 119 |
| 5 | 114597 | 35742 | 1020000 | 80 | 100 | 199503 | 265198 | 1 | 100.000 | 27 | 892 |
| 6 | 11649 | 880 | 5000000 | 27 | 4 | 199809 | 25482931 | 3 | 118.000 | 27 | 3274 |
| 7 | 930710 | 116005 | 5000000 | 16 | 100 | 197604 | 57138719 | 2 | 94.000 | 27 | 1210 |
| 8 | 59 | 11006 | 3500000 | 18 | 100 | 200711 | 20423389 | 5 | 111.000 | 4 | 94 |
| 9 | 57113 | 19549 | 16000000 | 27 | 4 | 201510 | 63710000 | 1 | 106.000 | 27 | 94 |
rand_values = generate_random_values(len(X_test))
rand_values = scaler.fit_transform(rand_values)
print("\nPredykcja dla losowych danych:\n")
y_pred = knn.predict(rand_values)
acc_sc = accuracy_score(y_test, y_pred)
print(f"KNN - wynik dokładności: {acc_sc}")
y_pred = logistic.predict(rand_values)
acc_sc = accuracy_score(y_test, y_pred)
print(f"Regresja logistyczna - wynik dokładności : {acc_sc}")
y_pred = svc.predict(rand_values)
acc_sc = accuracy_score(y_test, y_pred)
print(f"SVC - wynik dokładności: {acc_sc}")
y_pred = hybrid_method_predict(rand_values, X_train)
acc_sc = accuracy_score(y_test, y_pred)
print(f"Hybrydowa metoda - wynik dokładności: {acc_sc}")
Predykcja dla losowych danych: KNN - wynik dokładności: 0.4900662251655629 Regresja logistyczna - wynik dokładności : 0.5023651844843898 SVC - wynik dokładności: 0.5089877010406811 Hybrydowa metoda - wynik dokładności: 0.5070955534531694
Uzyskane wyniki predykcji - analiza¶
Dokładność uzyskanych wyników jest bliska 0.5, co sugeruje, że nasz model działa poprawnie, ponieważ dla zupełnie losowy zmiennych ma on tylko około 50% szansy na poprawną klasyfikację. Jest to prawdopobieństwo wybrania poprawnej odpowiedzi z dwóch (w wypadku projektu mamy albo "zły", albo "dobry" film), kierując się zupełną losowością, korzystając na przykład z rzutu monetą.
Podsumowanie¶
Porównanie wyników walidacji krzyżowej (0.70) z wynikami na losowych danych (około 0.5) pokazuje, że modele mogą skutecznie uczyć się i przewidywać na podstawie rzeczywistych danych zawierających wzorce, podczas gdy na danych losowych ich wydajność jest zbliżona do przypadkowego zgadywania.
Wnioski¶
Większość filmów należy do gorszej grupy, jedynie 10% z nich można nazwać "dobrymi". Osiągamy około 73% dokładności klasyfikacji.
Ilość filmów błędnie sklasyfikowanych jako dobre jest mniejsza niż sklasyfikowanych jako złe. Nie jest przesadnie szkodliwy fakt, ponieważ odrzucenie produkcji nic nie kosztuje w przeciewieństwie do nieudanej inwestycji w kiepski film.
Aby zwiększyć dokładność możnaby zwiększyść ilość danych, wykorzystać więcej metod, szczególnie takich, których efektywność wzrasta z ilością próbki uczącej. Metoda hybrydowa nieznacznie poprawia dokładność.
Bibliografia¶
Hosmer, Fred A. Jr., Stanley Lemeshow. (2004). A Primer on Logistic Regression. Journal of the National Cancer Institute 96, nr 2: 102-107. https://pubmed.ncbi.nlm.nih.gov/34952854
Mohseni, Mehrzad, Payam Ghahramani, Richard E. Fremouw. (2007). K-Nearest Neighbors for Multi-Label Classification. Proceedings of the 24th International Conference on Machine Learning. 1-2. https://www.jmlr.org/papers/volume25/23-0286/23-0286.pdf.
Joachim, Thorsten. (2011). Support Vector Machines. Applied Mathematics and Optimization 62, nr. 2: 443-474. https://link.springer.com/chapter/10.1007/0-387-25465-X_12.